package org.vaadin.nf4v.ext.config.menu;

import org.vaadin.nf4v.core.config.AbstractConfig;
import org.vaadin.nf4v.core.config.ContextConfig;
import org.vaadin.nf4v.core.config.ContextConfigurationException;
import org.vaadin.nf4v.core.config.PageConfig;
import org.vaadin.nf4v.core.config.process.NavigationConfigProcessor;
import org.vaadin.nf4v.core.config.process.parser.ContextConfigParser;
import org.vaadin.nf4v.core.config.process.parser.NavigationConfigParser;
import org.vaadin.nf4v.core.config.process.parser.PageConfigParser;
import org.vaadin.nf4v.ext.config.util.AnnotationUtil;
import org.vaadin.nf4v.ext.config.util.StringUtil;

import java.util.*;

/**
 * Configuration extension:
 * Scans window and page classes for @Menu configuration.
 * <p/>
 * Builds navigation tree for each context.
 * Pages may contain simple or hierarchical names to produce navigation tree, e.g.
 * "Administration/Control/Users" will be parsed as [Administration]->[Control]-[Users]
 * During parsing all hierarchical names are combined in complete navigation tree.
 * <p/>
 * Window may define only one level. This is useful to avoid prefixing all pages with the same group,
 * e.g. setting page menu as "Control/Users" and window menu as "Administration" will result
 * into "Administration/Control/Users" page menu path.
 * <p/>
 * The root node for navigation tree is stored in NavigationConfig and holds all context's menu trees.
 */
public class MenuAnnotationParser implements PageConfigParser, ContextConfigParser, NavigationConfigParser {
    public static final String MENU = "org.vaadin.nf4v.menu.MENU";


    ///////////////////////////////////////////////////////////////////////////////////// NAVIGATION PARSING

    @Override
    public void parse(NavigationConfigProcessor navigationConfigProcessor) throws ContextConfigurationException {
        MenuItemDescription root = new MenuItemDescription();
        for (ContextConfig context : navigationConfigProcessor.getNavigationConfig().getContextConfigs()) {
            MenuItemDescription menuItemDescription = ExtMenuUtil.getMenuItem(context);
            if (menuItemDescription != null) {
                root.getChilds().add(menuItemDescription);
            }
        }

        if (root.getChilds().size() > 0) {
            sortContexts(root);
            navigationConfigProcessor.getNavigationConfig().setAttribute(MENU, root);
        }
    }

    /**
     * Sort contexts root items by weight (more weight - deeper menu item) and name.
     * If context didn't have menu item name, context name is used
     * Override this to change context sorting.
     * Note: context trees sorting occurred in sortTree() method.
     *
     * @param root menu tree root
     */
    public void sortContexts(MenuItemDescription root) {
        Collections.sort(root.getChilds(), new Comparator<MenuItemDescription>() {
            @Override
            public int compare(MenuItemDescription o1, MenuItemDescription o2) {
                if (o1.getWeight() == o2.getWeight()) {
                    return (o1.getName()==null?o1.getContext():o1.getName())
                            .compareTo(o2.getName()==null?o2.getContext():o2.getName());
                } else {
                    return o1.getWeight() > o2.getWeight() ? 1 : -1;
                }
            }
        });
    }


    ///////////////////////////////////////////////////////////////////////////////////// CONTEXT PARSING

    @Override
    public void parse(NavigationConfigProcessor navigationConfigProcessor, ContextConfig contextConfig)
            throws ContextConfigurationException {
        parseItem(contextConfig, contextConfig.getWindowClass(), false);
        MenuItemDescription menuItem = contextConfig.getAttribute(MENU);
        if (menuItem == null) {
            // creating empty menu item just to aggregate page menu items (root of context menu tree)
            menuItem = new MenuItemDescription();
            contextConfig.setAttribute(MENU, menuItem);
        }
        menuItem.setContext(contextConfig.getName());

        buildContextNavigationTree(contextConfig);
    }

    /**
     * Builds menu items tree according to menu item paths.
     * Tree branches are sorted according to item name and weight.
     * Override this method to change menu tree building logic.
     *
     * @param config context configuration
     * @throws ContextConfigurationException on configuration errors
     */
    protected void buildContextNavigationTree(ContextConfig config) throws ContextConfigurationException {
        MenuItemDescription root = config.getAttribute(MENU);
        boolean isEmptyRoot = root.getName() == null;

        Map<String, MenuItemDescription> nodesIndex = new HashMap<String, MenuItemDescription>();
        if (!isEmptyRoot) {
            nodesIndex.put(root.getName().toLowerCase(), root);
        }
        for (PageConfig pageConfig : config.getPages()) {
            MenuItemDescription item = pageConfig.getAttribute(MENU);
            if (item != null) {
                MenuItemDescription currentRoot = root;

                // extending page path with context item
                boolean isPathRequireExtension = !isEmptyRoot && !root.getName().equals(item.getPath()[0]);
                if (isPathRequireExtension) {
                    String[] res = new String[item.getPath().length + 1];
                    System.arraycopy(item.getPath(), 0, res, 1, item.getPath().length);
                    res[0] = root.getName();
                    item.setPath(res);
                    item.setName(StringUtil.join(res, "/"));
                }

                for (int i = isPathRequireExtension ? 1 : 0; i < item.getPath().length; i++) {
                    String path = item.getPath()[i];

                    boolean isLastItem = i == (item.getPath().length - 1);

                    String[] fullPath = Arrays.copyOf(item.getPath(), i + 1);
                    String fullPathName = StringUtil.join(fullPath, "/");

                    MenuItemDescription aggregationRoot = nodesIndex.get(fullPathName.toLowerCase());
                    if (!isLastItem) {

                        // creating new grouping item (tree node)
                        if (aggregationRoot == null) {
                            aggregationRoot = new MenuItemDescription();
                            aggregationRoot.setName(fullPathName);
                            aggregationRoot.setContext(config.getName());
                            item.setPath(fullPath);

                            aggregationRoot.setParent(currentRoot);
                            currentRoot.getChilds().add(aggregationRoot);
                            nodesIndex.put(path.toLowerCase(), aggregationRoot);
                        }

                        currentRoot = aggregationRoot;

                    } else {
                        if (aggregationRoot != null) {
                            if (aggregationRoot.isCommandItem()) {
                                throw new ContextConfigurationException("Duplicate menu configuration entry " +
                                        item.getName() + " in page " + item.getConfig().getPageClass().getSimpleName() + ": " +
                                        "page " + aggregationRoot.getConfig().getPageClass().getSimpleName() +
                                        " is a already registered as leaf on this path");
                            } else {
                                /*
                                this situation occurred when some path, e.g.
                                Admin/Control/Users is already registered in tree
                                and current leaf is part of this path, e.g.
                                Admin/Control

                                This is absolutely valid situation, but fake node "Control" in tree
                                need to be replaced by current leaf item.
                                */
                                item.setParent(aggregationRoot.getParent());
                                item.setChilds(aggregationRoot.getChilds());
                                nodesIndex.put(item.getName().toLowerCase(), item);
                            }
                        }
                        item.setContext(config.getName());
                    }

                    currentRoot.getChilds().add(item);
                    item.setParent(currentRoot);
                }
                // remembering leaf
                // it is important cause it may be node for other page or it can be used to detect duplicate leafs
                nodesIndex.put(item.getName().toLowerCase(), item);
            }
        }

        sortTree(root);
    }

    /**
     * Sort context menu tree by weight (more weight - deeper menu item) and name.
     * Override this to change menu tree sorting.
     * Note: contexts sorting occurred in sortContexts() method.
     *
     * @param root menu tree root
     */
    protected void sortTree(MenuItemDescription root) {
        Collections.sort(root.getChilds(), new Comparator<MenuItemDescription>() {
            @Override
            public int compare(MenuItemDescription o1, MenuItemDescription o2) {
                if (o1.getWeight() == o2.getWeight()) {
                    return o1.getName().compareTo(o2.getName());
                } else {
                    return o1.getWeight() > o2.getWeight() ? 1 : -1;
                }
            }
        });
        for (MenuItemDescription item : root.getChilds()) {
            if (!item.isCommandItem()) {
                sortTree(item);
            }
        }
    }


    ///////////////////////////////////////////////////////////////////////////////////// PAGE PARSING

    @Override
    public void parse(NavigationConfigProcessor navigationConfigProcessor, ContextConfig contextConfig, PageConfig pageConfig)
            throws ContextConfigurationException {
        parseItem(pageConfig, pageConfig.getPageClass(), true);
    }

    /**
     * Searches for @Menu annotation and stores parsed menu item object as configuration attribute.
     *
     * @param config configuration
     * @param clazz  class to search annotation on
     * @param isPage true if current configuration is a page (applies additional restriction for non empty name)
     * @throws ContextConfigurationException on configuration error
     */
    protected void parseItem(AbstractConfig config, Class clazz, boolean isPage) throws ContextConfigurationException {
        Menu ann = AnnotationUtil.getClassAnnotation(Menu.class, clazz);
        if (ann != null) {
            MenuItemDescription menuItem = new MenuItemDescription();
            if (!StringUtil.isBlank(ann.name())) {
                menuItem.setName(ann.name());
                menuItem.setPath(parseMenuItemPath(ann.name()));
                if (!isPage && menuItem.getPath().length != 1) {
                    throw new ContextConfigurationException("Wrong context menu item configuration for context " +
                            ((ContextConfig) config).getName() + ": only one level path is allowed for context menu item");
                }
            } else {
                if (isPage) {
                    throw new ContextConfigurationException("Wrong page menu item configuration for page class " +
                            clazz.getSimpleName() + " : name is required");
                }
            }

            if (!StringUtil.isBlank(ann.icon())) {
                menuItem.setIcon(ann.icon());
            }

            menuItem.setWeight(ann.weight());
            menuItem.setSeparate(ann.separate());
            menuItem.setContextSensitive(ann.contextSensitive());

            if (isPage) {
                menuItem.setCommandItem(true);
                menuItem.setConfig((PageConfig) config);
            }

            config.setAttribute(MENU, menuItem);
        }
    }

    /**
     * Menu item name may represent a tree path, e.g. "Administration/Config/Users".
     * Method parses full menu name into path array (e.g. ["Administration", "Config", "Users"]
     * Override this method to change path parsing rules.
     * This may be useful for adding exceptions, e.g. when some page element contains "/" symbol in it's path
     * and should not be parsed.
     *
     * @param name menu item full name
     * @return menu item path
     * @throws ContextConfigurationException on configuration error
     */
    protected String[] parseMenuItemPath(String name) throws ContextConfigurationException {
        return StringUtil.split(name, "/");
    }
}
